package com.ibm.sova.abcd.cma.reqparser;

import com.ibm.sova.abcd.cma.RequestParser;
import com.ibm.sova.abcd.cma.dto.*;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.*;
import java.util.ArrayList;

import org.apache.log4j.Logger;
import org.apache.poi.hssf.usermodel.HSSFRow;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;

/**
 * This is an implementation of RequestParser interface, it is capable of parsing request data in XLS format. Apache POI is used to parsing XLS file. Logging is performed using log4j.
 * 
 * Thread Safety: it immutable and thread safe
 */
public class XLSRequestParser implements RequestParser {
    
    private final static int[] PART_NUM_LENGTHS = {4, 6, 7, 8};
    private final static String NAME_PART_NO = "Part No.";
    private final static String NAME_DESC = "Description";
    private final static String NAME_QUANTITY = "Quantity";
    private final static String SEVERITY_C= "C";
    private final static String SEVERITY_I = "I";
    private final static String MSG_PART_NO_MISS = "Header [Part No.] is missing";
    private final static String MSG_DESC_MISS = "Header [Description] is missing";
    private final static String MSG_QUANTITY_MISS = "Header [Quantity] is missing";
    /**
     * Represents the logger to perform logging.
     */
    private static Logger LOGGER = Logger.getLogger(XLSRequestParser.class);

    /**
     * Represents row number of the first data row (1-based).
     * 
     * It is initialized in ctor to a positive value, won't change afterwards, is used in parseRequestData method.
     */
    private int startDataRowNum;

    /**
     * Represents maximum count of data rows that can be processed.
     * 
     * It is initialized in ctor to positive value, won't change
     */
    private int maxDataRowCount;

    /**
     * Creates an instance of this class.
     * 
     * ###Params
     * startDataRowNum - row number of the first data row, should be greater than 1
     * maxDataRowCount - maximum number of data rows to process, should be positive
     * 
     * ###Impl
     * this.startDataRowNum = startDataRowNum;
     * this.maxDataRowCount = maxDataRowCount;
     * 
     * ###Exceptions
     * IllegalArgumentException - if any argument is invalid
     * @param startDataRowNum 
     * @param maxDataRowCount 
     */
    public XLSRequestParser(int startDataRowNum, int maxDataRowCount) {
        this.startDataRowNum = startDataRowNum;
        this.maxDataRowCount = maxDataRowCount;
    }

    /**
     * Parses request data and outputs an ABCDResponseData entity. 
     * 
     * It is possible that error might occur during parsing, all errors are stored in ABCDResponseData.overallMessages field, no exception is thrown.
     * 
     * ###Params
     * requestData - request data in byte array, cannot be null
     * 
     * ###Impl
     * 1. Create a ABCDResponseData object: result = new ABCDResponseData();
     * 2. Create a workbook: workbook = new HSSFWorkbook(new ByteArrayInputStream(requestData);
     * 3. Get the first sheet: sheet = workbook.getSheetAt(0);
     * 4. Get the first row: row = sheet.getRow(0);
     * 5. if row.getCell(0).getStringCellValue() != "Part No.":
     *     5.1 Create an ErrorMessage object: msg=new ErrorMessage();
     *     5.2 Set error message: msg.severity="C"; msg.message="Header [Part No.] is missing";
     *     5.3 Add error message to response data: result.overallMessages.add(msg); result.criticalErrors=true;
     * 6. if row.getCell(2).getStringCellValue() != "Description":
     *     6.1 Create an ErrorMessage object: msg=new ErrorMessage();
     *     6.2 Set error message: msg.severity="C"; msg.message="Header [Description] is missing";
     *     6.3 Add error message to response data: result.overallMessages.add(msg); result.criticalErrors=true;
     * 7. if row.getCell(3).getStringCellValue() != "Quantity":
     *     7.1 Create an ErrorMessage object: msg=new ErrorMessage();
     *     7.2 Set error message: msg.severity="C"; msg.message="Header [Quantity] is missing";
     *     7.3 Add error message to response data: result.overallMessages.add(msg); result.criticalErrors=true;
     * 8. Declare data row counter: rowCount = 0;
     * 9. Declare current product config: ProductConfiguration currentProductConfig=null;
     * 9. For i=startDataRowNum-1; i<sheet.getLastRowNum(); i++:
     *     9.1 If sheet.getRow(i) == null: currentProductConfig=null; continue;
     *     9.2 If isValidPartNo(sheet.getRow(i).getCell(0).getStringCellValue()) is true:
     *           9.2.1 Increment row counter: rowCount++;
     *           9.2.2 If rowCount>maxDataRowCount:
     *                    9.2.2.1 Create a ErrorMessage: errorMsg=new ErrorMessage();
     *                    9.2.2.2 Set severity: errorMsg.severity="I";
     *                    9.2.2.3 Set error message: errorMsg.message="There are more than "+maxDataRowCount+" rows in input XLS file";
     *                    9.2.2.4 Add error message to result: result.overallMessages.add(errorMsg);
     *                    9.2.2.5 Break the loop;
     *           9.2.3 If currentProductConfig == null: currentProductConfig=new ProductConfig(); result.productConfig.add(currentProductConfig);
     *           9.2.4 Create a Product entity: product=new Product();
     *           9.2.5 Set product id: product.id=sheet.getRow(i).getCell(0).getStringCellValue();
     *           9.2.6 Set description: product.description=sheet.getRow(i).getCell(2).getStringCellValue();
     *           9.2.7 Set quantity: product.quantity=sheet.getRow(i).getCell(3).getNumericCellValue();
     *                    9.2.7.1 If an exception is thrown in this step, catch it:
     *                                9.2.7.1.1 Create a error message: errorMsg=new ErrorMessage();
     *                                9.2.7.1.2 Set error severity: errorMsg.severity="C";
     *                                9.2.7.1.3 Set error message: errorMsg.message="Data in row "+i+" column 3 should be a number";
     *                                9.2.7.1.4 Add error message to result: result.overallMessages.add(errorMsg); result.criticalErrors=true;
     *           9.2.8 Add product to current product config: currentProductConfig.products.add(product);
     * 10. return result;
     * 
     * Note, if any other exception (not IAE) is thrown, catch it and run following logic:
     * ABCDResponseData response=new ABCDResponseData();
     * response.criticalErrors=true;
     * ErrorMessage errMsg=new ErrorMessage();
     * errMsg.severity="C";
     * errMsg.message="Request XLS data file is corrupted, cannot be parsed, inner error is [" + <exception stacktrace> + "]";
     * return response;
     * 
     * ###Exceptions
     * IllegalArgumentException - if any argument is invalid
     * CMARequestParsingException - if there is an error when parsing request data
     * @param requestData 
     * @return 
     */
    public ABCDResponseData parseRequestData(byte[] requestData) {
        ABCDResponseData result = new ABCDResponseData();
        try {
            HSSFWorkbook workbook = new HSSFWorkbook(new ByteArrayInputStream(requestData));
            //check null
            HSSFSheet sheet = workbook.getSheetAt(0);
            //check null
            HSSFRow row = sheet.getRow(0);
            //check null
            if (!row.getCell(0).getStringCellValue().equals(NAME_PART_NO)) {
                addErrorMessageToResponse(result, SEVERITY_C, MSG_PART_NO_MISS, true);
            } else if(!row.getCell(0).getStringCellValue().equals(NAME_DESC)) {
                addErrorMessageToResponse(result, SEVERITY_C, MSG_DESC_MISS, true);
            } else if(!row.getCell(0).getStringCellValue().equals(NAME_QUANTITY)) {
                addErrorMessageToResponse(result, SEVERITY_C, MSG_QUANTITY_MISS, true);
            }
            int rowCount = 0;
            ProductConfiguration currentProductConfig = null;
            for(int i = startDataRowNum - 1; i<sheet.getLastRowNum(); i++) {
                if (sheet.getRow(i) == null) {
                    currentProductConfig=null;
                    continue;
                }
                if (isValidPartNo(sheet.getRow(i).getCell(0).getStringCellValue())) {
                    rowCount++;
                    if (rowCount > maxDataRowCount) {
                        addErrorMessageToResponse(result, SEVERITY_I, "There are more than " + maxDataRowCount + " rows in input XLS file", false);
                        break;   
                    }
                }
                if (currentProductConfig == null) {
                    currentProductConfig = new ProductConfiguration();
                    if(result.getProductConfig() == null) {
                        result.setProductConfig(new ArrayList());
                    }
                    result.getProductConfig().add(currentProductConfig);
                }
                Product product = new Product();
                product.setId(sheet.getRow(i).getCell(0).getStringCellValue());
                product.setDescription(sheet.getRow(i).getCell(2).getStringCellValue());
                try {
                    product.setQuantity(Double.valueOf(sheet.getRow(i).getCell(3).getNumericCellValue()).intValue());
                } catch (Exception e) {
                    addErrorMessageToResponse(result, SEVERITY_C, "Data in row " + i + " column 3 should be a number", true);
                }
                if(currentProductConfig.getProducts() == null) {
                    currentProductConfig.setProducts(new ArrayList());
                }
                currentProductConfig.getProducts().add(product);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        //exception handling
        return result;
    }
    
    /**
     * <p>
     * add Critical Error Message To Response Message
     * </p>
     * @param responseData
     *          ABCDResponseData
     * @param msgContent
     *          message Content
     */
    private void addErrorMessageToResponse(ABCDResponseData responseData, String severity, String msgContent, boolean isCriticalError) {
        ErrorMessage msg = new ErrorMessage();
        msg.setSeverity(severity);
        msg.setMessage(msgContent);
        if (responseData.getOverallMessages() == null) {
            responseData.setOverallMessages(new ArrayList());
        }
        responseData.getOverallMessages().add(msg);
        if(isCriticalError) {
            responseData.setCriticalErrors(isCriticalError);
        }
    }

    /**
     * Check whether a Part No is correct.
     * 
     * ###Params
     * partNo - the Part No to check
     * return - true if it is well-formed Part No, otherwise false
     * 
     * ###Impl
     * If (length of partNo is 4 or 6 or 7 or 8) AND (partNo only contains alphanumeric characters): return true;
     * Else: return false;
     * 
     * ###Exceptions
     * None
     * @param partNo
     *          the Part No to check
     * @return true if it is well-formed Part No, otherwise false
     */
    private boolean isValidPartNo(String partNo) {
        if (validLength(partNo.length()) && partNo.matches("^[0-9a-zA-Z ]+$")) {
            return true;
        }
        return false;
    }

    /**
     * <p>
     * Check length against predefined length array.
     * </p>
     * @param length
     *          length
     * @return true if it is predefined length, otherwise false
     */
    private boolean validLength(int length) {
        for (int i = 0; i < PART_NUM_LENGTHS.length; i++) {
            if (length == PART_NUM_LENGTHS[i]) {
                return true;
            }
        }
        return false;
    }
}

